% db.erl
%
% Main function that contains the code to set up neighbors and how each node
% will handle incoming messages from other nodes
%
-module(db).
-export([createNeighborList/2, joinNeighborLists/2, startServers/2, stopServers/1, add/3, del/2, dump/1, getMax/1, getMin/1, flush/1, stop/1]).
-import(erlang, [display/1]).

% Exported helper functions.  Shouldn't be used by clients
-export([addNeighbors/2, createNeighborList_help/4, getMin_help/2, getMax_help/2, startServers_help/3, loop/1, processMsg/2, getMinFloatFromListSet/3, getMaxFloatFromListSet/3]).

% iterate through the server list and assign each node a list of nieghbors based on a matrix pattern.
createNeighborList(Start, Servers) ->
    ListLength = lists:flatlength(Servers),
    createNeighborList_help(Start, ListLength, Servers, []).

createNeighborList_help(Start, ListLength, _Servers, NeighborList) when Start == ListLength + 1 ->
    NeighborList;
createNeighborList_help(Start, ListLength, Servers, NeighborList) ->
    MatrixSize = round(math:sqrt(ListLength)),
    MatrixMod = Start rem MatrixSize,
    case MatrixMod /= 1 of
        true ->
            MinusOne = appendNeighborList([lists:nth(Start - 1 , Servers) | NeighborList]);
        false -> MinusOne = []
    end,
    case MatrixMod /= 0 of
        true ->
           case Start + 1 =< ListLength of
            true ->
                PlusOne = appendNeighborList([lists:nth(Start + 1 , Servers) | MinusOne]);
            false -> PlusOne = MinusOne
           end;
        false -> PlusOne = MinusOne
    end,  
    case MatrixMod =:=1 orelse MatrixMod /=0 of
        true ->
            case Start + MatrixSize + 1 =< ListLength of
                true -> 
                        MatrixSizePlusOne = appendNeighborList([lists:nth((Start + MatrixSize + 1), Servers) | PlusOne]);
                false -> MatrixSizePlusOne = PlusOne
            end;
        false -> MatrixSizePlusOne = PlusOne
    end,   
    case MatrixMod /=1 orelse MatrixMod =:=0 of
        true ->
            case (Start + MatrixSize - 1) =< ListLength of
                true ->
                        MatrixSizeMinusOne = appendNeighborList([lists:nth((Start + MatrixSize - 1), Servers) | MatrixSizePlusOne]);
                false -> MatrixSizeMinusOne = MatrixSizePlusOne
            end;
        false -> MatrixSizeMinusOne = MatrixSizePlusOne
    end,    
    case MatrixMod =:=1 orelse MatrixMod /=0 of
        true ->
            case (Start - MatrixSize + 1) > 0 of
                true ->
                       MinusMatrixSizePlusOne = appendNeighborList([lists:nth((Start - MatrixSize + 1), Servers) | MatrixSizeMinusOne]);
                false -> MinusMatrixSizePlusOne = MatrixSizeMinusOne
            end;
        false -> MinusMatrixSizePlusOne = MatrixSizeMinusOne
    end,  
    case MatrixMod /=1 orelse MatrixMod =:=0 of
        true ->
            case (Start - MatrixSize - 1) > 0 of
                true ->
                        PlusMatrixSizeMinusOne = appendNeighborList([lists:nth((Start - MatrixSize - 1), Servers) | MinusMatrixSizePlusOne]);
                false -> PlusMatrixSizeMinusOne = MinusMatrixSizePlusOne
            end;
        false -> PlusMatrixSizeMinusOne = MinusMatrixSizePlusOne
    end,               
    case Start + MatrixSize =< ListLength of
        true ->
            PlusMatrixSize = appendNeighborList([lists:nth(Start + MatrixSize, Servers) | PlusMatrixSizeMinusOne]);
        false -> PlusMatrixSize = PlusMatrixSizeMinusOne
    end,
    case Start - MatrixSize > 0 of
        true ->
            MinusMatrixSize = appendNeighborList([lists:nth(Start - MatrixSize, Servers) | PlusMatrixSize]);
        false -> MinusMatrixSize = PlusMatrixSize
    end,
    Server = lists:nth(Start, Servers),
    Server ! {addn, hd(MinusMatrixSize)},
    addNeighbors(Server, tl(MinusMatrixSize)),
    createNeighborList_help(Start + 1, ListLength, Servers, NeighborList).

addNeighbors(_Server, [])->
    ok;
addNeighbors(Server, NeighborList) ->
    Server ! {addn, hd(NeighborList)},
    addNeighbors(Server, tl(NeighborList)).

appendNeighborList(NeighborList) ->
    NeighborList.
    
% starts multiple instances of a server
startServers(Num, Offset) ->
    Created = 1 + Offset,
	startServers_help(Num, Created, []).

startServers_help(Num, Created, List) when Num == Created - 1 ->
	List;
startServers_help(Num, Created, List) ->
	Id = list_to_atom("server" ++ integer_to_list(Created)),
	Name = {Id, node()},
	start(Id),
	startServers_help(Num, Created + 1, [ Name | List ]).
	 	
% Stops a list of servers
stopServers([]) ->
	ok;
stopServers(Servers) ->
	hd(Servers) ! quit,
	stopServers(tl(Servers)).
    
% addn to first and last in list
joinNeighborLists(Servers, NumServers) ->
    ServerFirst = lists:nth(1, Servers),
    ServerFirst ! {addn, lists:nth(NumServers, Servers)},
    ServerLast = lists:nth(NumServers, Servers),
    ServerLast ! {addn, lists:nth(1, Servers)},
    Servers.
    
% Gets the minimum value from all the servers
getMin(Servers) ->
	getMin_help(Servers, 1.0E45).

getMin_help([], Min) ->
	Min;
getMin_help(Servers, Min) ->
	MinServer = getMinFromServer(hd(Servers)),
	if
		MinServer < Min ->
			getMin_help(tl(Servers), MinServer);
		true ->
			getMin_help(tl(Servers), Min)
	end.

% Gets the maximum value from all the servers
getMax(Servers) ->
	getMax_help(Servers, -1.0E45).

getMax_help([], Max) ->
	Max;
getMax_help(Servers, Max) ->
	MaxServer = getMaxFromServer(hd(Servers)),
	if
		MaxServer > Max ->
			getMax_help(tl(Servers), MaxServer);
		true ->
			getMax_help(tl(Servers), Max)
	end.
    
% Starts a single instance of the server with the given Id
start(Id) ->
	Dict = dict:new(),
	NeighborNodes = [],
	DataStruct = {Id, Dict, NeighborNodes, average:new(), median:new(), 0, 0},
	register(Id, spawn(db, loop, [DataStruct])),
	ok.

% client functions to pass a message to the given server
add(Id, Tag, Float) ->
	Id ! {add, Tag, Float},
	ok.
	
del(Id, Tag) ->
	Id ! {del, Tag},
	ok.

getMaxFromServer(Id) ->
	Id ! {max, self()},
	receive
		Float when is_float(Float) ->
			Float
	end.

getMinFromServer(Id) ->
	Id ! {min, self()},
	receive
		Float when is_float(Float) ->
			Float
	end.

flush(Id) ->
	Id ! flush,
	ok.

dump(Id) ->
	Id ! {dump, self()},
	receive
		Dict ->
			Dict
	end.

stop(Id) ->
	Id ! quit,
	ok.

% Message loop for retrieving, processing, and stopping the server
loop(DataStruct) ->
	receive
		quit ->
			%io:format("~p\tShutting down~n", [element(1, DataStruct)]),
			ok;
		Msg ->
			loop(processMsg(Msg, DataStruct))
	end.

% main processing function that for min, max, avg, median, update and retrieve
processMsg(Msg, DataStruct) ->
	% variables needed
	Id = element(1, DataStruct),
	Dict = element(2, DataStruct),
	NeighborNodes = element(3, DataStruct),
	AvgStruct = element(4, DataStruct),
	MedianStruct = element(5, DataStruct),
	FloatVal = element(6, DataStruct),
	Bool = element(7, DataStruct),

	case Msg of
		{add, Tag, Float} when is_float(Float) ->
			%io:format("~p\tAdding a float~p~n", [Id, Float]),	
			RetVal = {Id, dict:append(Tag, Float, Dict), NeighborNodes, AvgStruct, MedianStruct, FloatVal, Bool};
		{add, Tag, FloatList} when is_list(FloatList) ->
			case dict:is_key(Tag, Dict) of
				true ->
					%io:format("~p\tKey ~p already exists", [Id, Tag]),
					RetVal = DataStruct;
				_Other ->
					%io:format("~p\tAdding list of floats~n", [Id]),
					RetVal = {Id, dict:append_list(Tag, FloatList, Dict), NeighborNodes, AvgStruct, MedianStruct, FloatVal, Bool}
			end;
		{addn, Node} when is_tuple(Node) ->
			%io:format("~p\tAdding a neighbor node~p~n", [Id, Node]),
			RetVal = {Id, Dict, [Node | NeighborNodes], AvgStruct, MedianStruct, FloatVal, Bool};
		{max, Proc} ->
			%io:format("~p Getting maximum value~n", [Id]),	
			Proc ! getMaxFloatFromListSet(dict:fetch_keys(Dict), Dict, -1.0E45),
			RetVal = DataStruct;
		{min, Proc} ->
			%io:format("~p Getting minimum value~n", [Id]),
			Proc ! getMinFloatFromListSet(dict:fetch_keys(Dict), Dict, 1.0E45),
			RetVal = DataStruct;
		{dump, Proc} ->
			Proc ! DataStruct,
			RetVal = DataStruct;
            
        % methods for calculating an median
        {median, start} ->
            NewMedianStruct = median:addFragmentsToMedianStruct(dict:fetch_keys(Dict), Dict, MedianStruct),
            spawn(median, start, [Id, NeighborNodes, NewMedianStruct, Dict]),
            RetVal = {Id, Dict, NeighborNodes, AvgStruct, NewMedianStruct, FloatVal, Bool};
        {median, ReceivedMedianStruct, Proc} ->
            NewMedianStruct1= median:addFragmentsToMedianStruct(dict:fetch_keys(Dict), Dict, MedianStruct),
            NewMedianStruct2= median:merge(NewMedianStruct1, ReceivedMedianStruct),
            Proc ! NewMedianStruct2,
            RetVal = {Id, Dict, NeighborNodes, AvgStruct, NewMedianStruct2, FloatVal, Bool};    
        {getmedian, Proc} ->
            Proc ! median:getList(MedianStruct),
            RetVal = DataStruct;      
            
        % methods for calculating an average
		{avg, start} ->
			NewAvgStruct = average:incrTimeStamp(1, average:addFragmentsToAvgStruct(dict:fetch_keys(Dict), Dict, AvgStruct)),
			
			spawn(average, start, [Id, NeighborNodes, NewAvgStruct]),
			RetVal = {Id, Dict, NeighborNodes, NewAvgStruct, MedianStruct, FloatVal, Bool};

		{avg, ReceivedAvgStruct, Proc} ->
		
		  % Add my keys to my average struct
		  NewAvgStruct1 = average:addFragmentsToAvgStruct(dict:fetch_keys(Dict), Dict, AvgStruct),
		  
		  % Merge my average structure with that which was given to me
			NewAvgStruct = average:merge(NewAvgStruct1, ReceivedAvgStruct),
			
			Proc ! NewAvgStruct,
			
			% Set the return value of this function to the new average structure
			RetVal = {Id, Dict, NeighborNodes, NewAvgStruct, MedianStruct, FloatVal, Bool};
		{getavg, Proc} ->
			Avg = average:getCount(AvgStruct),
			if
				Avg > 0 ->
					Proc ! {average:getSum(AvgStruct) / Avg, average:getTimeStamp(AvgStruct)};
				true ->
					Proc ! unknown
			end,
			RetVal = DataStruct;
		% methods for updating a fragment
		{update, Tag, FloatList} ->
			% First, update my own dictionary
			if
			   Bool == 0 ->
			      case dict:is_key(Tag, Dict) of
			         true ->
					io:format("Updating fragment number: ~p for ~p~n", [Tag, Id]),
					NewDict = dict:append_list(Tag, FloatList, dict:erase(Tag, Dict)),
					RetVal = {Id, NewDict, NeighborNodes, AvgStruct, MedianStruct, FloatVal, 1},
					spawn(fileupdate, start, [Id, NeighborNodes, Tag, FloatList]);
				_Other ->
					RetVal = {Id, Dict, NeighborNodes, AvgStruct, MedianStruct, FloatVal, 1},
					spawn(fileupdate, start, [Id, NeighborNodes, Tag, FloatList])
			      end;
                           true ->
			      RetVal = DataStruct
                        end;
		{checkfragment, Tag} ->
			case dict:is_key(Tag, Dict) of
			         true -> io:format("~p has fragment ~p~n", [Id, Tag]);
				 _Other -> ok
			end,
			RetVal = DataStruct;
		{sizeN, Proc} ->
		  	Proc ! length(NeighborNodes),
		  	RetVal = DataStruct;
		% Methods for retrieving a fragment
		{retrieve, Tag, Proc} ->
		  if
	             Bool == 0 ->
	                % Check if I have the fragment
		        case dict:is_key(Tag, Dict) of
		           true ->
		              Proc ! {Tag, dict:fetch(Tag, Dict), Id},
		              RetVal = {Id, Dict, NeighborNodes, AvgStruct, MedianStruct, FloatVal, 1};
		           _Other ->
		              spawn(fileretrieve, start, [NeighborNodes, Tag, Proc]),
		              RetVal = {Id, Dict, NeighborNodes, AvgStruct, MedianStruct, FloatVal, 1}
		        end;
                      true ->
			      RetVal = DataStruct
                  end; 
		% resets boolean variable to 0
		{reset} ->
			RetVal = {Id, Dict, NeighborNodes, AvgStruct, MedianStruct, FloatVal, 0};
		% handles creating new process for gossip min
		{gmin} ->
			Val = getMinFloatFromListSet(dict:fetch_keys(Dict), Dict, 1.0E45),
			spawn(gossipMin, start, [Id, NeighborNodes, Val]),
			RetVal = {Id, Dict, NeighborNodes, AvgStruct, MedianStruct, Val, Bool};
		% compares received min and own and updates accordingly
		{gmincomp, NewMin, Proc} ->
			MyMin = FloatVal,
			if
				MyMin < NewMin ->
					Smin = MyMin;
				true ->
					Smin = NewMin
			end,
			%io:format("gmincomp: ~p's recv min: ~p new min val: ~p~n", [Id, NewMin, Smin]),
			Proc ! Smin,
			RetVal = {Id, Dict, NeighborNodes, AvgStruct, MedianStruct, Smin, Bool};
		% handles creating new process for gossip max
		{gmax} ->
			Val = getMaxFloatFromListSet(dict:fetch_keys(Dict), Dict, -1.0E45),
			spawn(gossipMax, start, [Id, NeighborNodes, Val]),
			RetVal = {Id, Dict, NeighborNodes, AvgStruct, MedianStruct, Val, Bool};
		% compares received max and own and updates accordingly
		{gmaxcomp, NewMax, Proc} ->
			MyMax = FloatVal,
			if
				MyMax > NewMax ->
					Smax = MyMax;
				true ->
					Smax = NewMax
			end,
			Proc ! Smax,
			RetVal = {Id, Dict, NeighborNodes, AvgStruct, MedianStruct, Smax, Bool};
		{printMin} ->
			io:format("FINAL: ~p's min val: ~p~n", [Id, FloatVal]),
			RetVal = DataStruct;
		{printMax} ->
			io:format("FINAL: ~p's max val: ~p~n", [Id, FloatVal]),
			RetVal = DataStruct;
		Msg ->
			io:format("~p\tUnknown message ~p~n", [Id, Msg]),
			RetVal = DataStruct
	end,
	RetVal.

% calculates the min value the process
getMinFloatFromListSet([], _Dict, Min) ->
    Min;
getMinFloatFromListSet(Keys, Dict, Min) ->
    Key = hd(Keys),
    Values = dict:fetch(Key, Dict),
    ListMin = lists:min(Values),
    if
        ListMin < Min ->
            getMinFloatFromListSet(tl(Keys), Dict, ListMin);
        true ->
            getMinFloatFromListSet(tl(Keys), Dict, Min)
    end.

% calculates the max value the process    
getMaxFloatFromListSet([], _Dict, Max) ->
    Max;
getMaxFloatFromListSet(Keys, Dict, Max) ->
    Key = hd(Keys),
    Values = dict:fetch(Key, Dict),
    ListMax = lists:max(Values),
    if
        ListMax > Max ->
            getMaxFloatFromListSet(tl(Keys), Dict, ListMax);
        true ->
            getMaxFloatFromListSet(tl(Keys), Dict, Max)
    end.
